

import types
from datetime import date
from dateutil.parser import parse as dateparse

from PyQt4.QtCore import QVariant, QString, QDate

class SCRecordError(Exception):
    pass

class SCDataAtom(object):
    '''Keep atomic data in standard python formats.
    '''
    
    #LABELS_ID = {'vert': u'|', 'horiz': u'_', 'both': u'#'}
    LABELS_ID = {'|': 'vert', '_': 'horiz', '#' : 'both'}
    INV_LABELS_ID = {'vert':'|', 'horiz':'_','both':'#'}
    HINT_CHAR = ':'
    AKA_CHAR = '@'
    FLOAT_TYPES = [types.FloatType, ]
    INT_TYPES = [types.IntType,]
    
    try:
        import numpy
        FLOAT_TYPES.append(numpy.float64)
        FLOAT_TYPES.append(numpy.float32)
        INT_TYPES.append(numpy.int32)
    except ImportError:
        pass
    
    def __init__(self,atom = None, locked = False):
        self.__atom = atom
        self.__raw = atom
        self.__locked = locked
        self.__direction = None
        self.__shapeHint = ''
        self.__akaName = ''
        self.__type = None
        self.__processAtom__()
        self.__processType__()
    
    def __processAtom__(self):
        if isinstance(self.__atom,basestring) and \
            len(self.__atom) > 1 and \
            (self.__atom[0] in map(None,SCDataAtom.LABELS_ID.iterkeys())):
            self.__direction = SCDataAtom.LABELS_ID[self.__atom[0]]
            self.__atom = self.__atom[1:]
            tmp = self.__atom.split(SCDataAtom.HINT_CHAR)
            if len(tmp) > 1 and tmp[-1]:
                self.__shapeHint = tmp[-1]
                self.__atom = tmp[0]
                if '@' in self.__shapeHint:
                    self.__akaName = self.__shapeHint.replace('@','')
        if type(self.__atom) in SCDataAtom.FLOAT_TYPES:
            self.__atom = float(self.__atom)
        if type(self.__atom) in SCDataAtom.INT_TYPES:
            self.__atom = int(self.__atom)

    def __processType__(self):
        if isinstance(self.__atom,date):
            self.__type = 'date'
        elif type(self.__atom) is types.BooleanType:
            self.__type = 'bool'
        elif isinstance(self.__atom,basestring):
            if self.__direction in SCDataAtom.LABELS_ID.values():
                self.__type = 'label'
            elif self.__atom[0] == '=':
                self.__type = 'expr'
            else:
                self.__type = 'string'
        elif type(self.__atom) in SCDataAtom.FLOAT_TYPES:
            self.__type = 'float'
        elif type(self.__atom) in SCDataAtom.INT_TYPES:
            self.__type = 'int'

    def __repr__(self):
        return repr(self.__atom)
    
    def __str__(self):
        if self.imDate():
            return self.__atom.strftime('%d/%m/%Y')
        return str(self.__atom)

    def __getData__(self):
        return self.__atom
    data = property(__getData__)
    
    def __getDirection__(self):
        '''Return the direction (None if it's not a label).
        '''
        return self.__direction
    direction = property(__getDirection__)
    
    def __getShapeHint__(self):
        '''Return the shape hint for the label data
        '''
        return self.__shapeHint
    shapeHint = property(__getShapeHint__)

    def __getAkaName__(self):
        '''Return the alias name for the label'''
        return self.__akaName
    akaName = property(__getAkaName__)

    def __getRawData__(self):
        return self.__raw
    rawData = property(__getRawData__)

    def __getLocked__(self):
        return self.__locked
        
    def __setLocked__(self,locked):
        self.__locked = locked

    locked = property(__getLocked__,__setLocked__)
        
    def imNone(self):
        return self.__atom is None
    
    def imDate(self):
        return self.__type == 'date'
    
    def imBool(self):
        return self.__type == 'bool'
    
    def imString(self):
        return self.__type in ('string','label','expr')

    def imFloat(self):
        return self.__type == 'float'

    def imInt(self):
        return self.__type == 'int'

    def imLabel(self, dir='any'):
        '''
        @param dir, direction can be 'any, vert, horiz, both'
        '''
        if dir == 'any':
            return self.__type == 'label'
        return self.__direction == dir

    def imNumber(self):
        return self.imInt() or self.imFloat()

    def imExpr(self):
        return self.__type == 'expr'

    def imAKA(self):
        if not self.imLabel():
            return False
        if isinstance(self.__shapeHint,basestring):
            return (SCDataAtom.AKA_CHAR in self.__shapeHint)
        return False

class SCDataAtomHelper(object):
    ''''''
    def __init__(self,workspace = {}):
        self.__workspace = workspace
    
    def setWorkspace(self,workspace):
        self.__workspace = workspace

    def evalAtom(self,anAtom):
        if anAtom.imExpr():
            return eval(anAtom.data[1:],self.__workspace)
        if anAtom.imLabel():
            return anAtom.rawData
        return anAtom.data
    
    def atomFromQVariant(self,aVariant):
        '''Returns a SCDataAtom instance from a QVariant.
        @param aVariant, a QVariant
        '''
        if self.isNone(aVariant):
            return SCDataAtom()
        elif self.isQDate(aVariant):
            tmp = aVariant.toDate()
            return SCDataAtom(date(tmp.year(),tmp.month(),tmp.day()))
        elif self.isDateStr(aVariant):
            return SCDataAtom(dateparse(str(aVariant.toString()),
                                        dayfirst=True).date())
        elif self.isBool(aVariant):
            return SCDataAtom(aVariant.toBool())            
        elif self.isInt(aVariant):
            return SCDataAtom(aVariant.toInt()[0])
        elif self.isFloat(aVariant):
            return SCDataAtom(aVariant.toDouble()[0])
        elif self.isOther(aVariant):
            return SCDataAtom(eval(str(aVariant.toString())))
        elif self.isExpr(aVariant):
            return SCDataAtom(str(aVariant.toString()))
        elif self.isInputExpr(aVariant):
            value = eval(str(aVariant.toString()[2:]),self.__workspace)
            return SCDataAtom(value)
        return SCDataAtom(str(aVariant.toString()))

    def atomToQString(self,anAtom):
        if anAtom.imDate():
            return QString(anAtom.data.strftime('%d/%m/%Y'))
        elif anAtom.imNone():
            return QString('')
        return QString(str(anAtom.rawData))
    
    def atomToQVariant(self,anAtom,truncFloat=False):
        return QVariant(self.atomToNearestQObject(anAtom,truncFloat=truncFloat))
    
    def atomToNearestQObject(self,anAtom,rec=0,truncFloat=False):
        '''Build the Qt Object most similar to base atomic data.
        '''
        if anAtom.imDate():
            return QDate(anAtom.data.year,anAtom.data.month,anAtom.data.day)
        elif anAtom.imExpr():
            try:
                assert rec < 2, 'Too long recursion reached.'
                atom = SCDataAtom(eval(anAtom.data[1:],self.__workspace))
                return self.atomToNearestQObject(atom,rec+1,truncFloat)
            except Exception:
                return anAtom.data
        elif anAtom.imNone():
            return QString('')
        elif anAtom.imFloat() and truncFloat:
            if abs(anAtom.data) < 1.0e-3 and \
                not anAtom.data == 0.:
                return QString('%.3e' % anAtom.data)
            else:
                return QString('%.3f' % anAtom.data)
        return anAtom.data

    def isQDate(self,aVariant):
        return aVariant.toDate().isValid()
    
    def isDateStr(self,aVariant):
        try:
            assert not self.isNumber(aVariant)
            assert aVariant.toString().size() > 1
            assert not aVariant.toString().isEmpty()
            tmp = dateparse(str(aVariant.toString()),dayfirst=True)
            assert tmp.year > 1900
            return True
        except:
            return False

    def isBool(self,aVariant):
        return aVariant.toString().toLower() in ('false','true')

    def isNumber(self,aVariant):
        return aVariant.toDouble()[1]
    
    def isFloat(self,aVariant):
        return self.isNumber(aVariant)

    def isInt(self,aVariant):
        if not self.isNumber(aVariant):
            return False
        if not aVariant.toInt()[1]:
            return False
        a = aVariant.toDouble()[0]
        b = aVariant.toInt()[0]
        return (abs(a-b) < 1.0e-8)

    def isNone(self,aVariant):
        return aVariant.toString().isEmpty()
    
    def isOther(self,aVariant):
        return False
        try:
            eval(str(aVariant.toString()))
            return True
        except Exception:
            return False

    def isExpr(self,aVariant):
        if not aVariant.toString().startsWith('='):
            return False
        try:
            eval(str(aVariant.toString()[1:]),self.__workspace)
            return True
        except Exception:
            return False
    
    def isInputExpr(self,aVariant):
        if not aVariant.toString().startsWith(':='):
            return False
        try:
            eval(str(aVariant.toString()[2:]),self.__workspace)
            return True
        except Exception:
            return False

    def isButton(self,aVariant):
        if aVariant.toString().startsWith('@'):
            return True
        return False
